﻿using System;
using System.Diagnostics;
using System.Linq;
using System.Threading;
using System.Threading.Tasks;

namespace Gems
{
    public class HealthMonitor
    {
        readonly SuspectMatrix _suspectMatrix;
        readonly IUnicastLink _unicastLink;
        readonly int _nodeCount;
        readonly Timer _tickTimer;
        readonly IConfiguration _configuration;
        readonly System.Random _random;
        private readonly bool[] _liveList;

        public HealthMonitor(IConfiguration configuration)
        {
            Traces.Gems.TraceEvent(TraceEventType.Information, Traces.StartingHealthMonitor, "Starting health monitor.");
            _nodeCount = configuration.Hosts.Length;
            _suspectMatrix = new SuspectMatrix(_nodeCount, configuration.HostPosition, configuration.GossipTimeout, configuration.UpdateTimeout);
            _suspectMatrix.Agreed += new Action<int>(SuspectMatrixAgreed);

            _unicastLink = new UdpUnicastLink(configuration.GossipPort);
            _unicastLink.Received += new OnDataReceived(UnicastLinkReceived);

            _configuration = configuration;
            _random = new Random(System.Environment.TickCount);
            _liveList = (from i in Enumerable.Range(0, configuration.Hosts.Length) select true).ToArray();
            
            _tickTimer = new Timer(tickTimerElapsed, null, 0, System.Threading.Timeout.Infinite);

            Traces.Gems.TraceEvent(TraceEventType.Information, Traces.HealthMonitorStarted, "Health monitor started.");

        }

        private void SuspectMatrixAgreed(int nodeNumber)
        {
            Traces.Gems.TraceEvent(TraceEventType.Warning, Traces.NodeFailed, "Node '{0}' has failed.",
                                       _configuration.Hosts[nodeNumber]);
            _liveList[nodeNumber] = false;

            if (!_liveList.Where((b, i) => i != _configuration.HostPosition).Any(b => b))
            {
                SuspendGossips();
                Traces.Gems.TraceEvent(TraceEventType.Information, Traces.GossipSuspended, "No live nodes left. Gossips suspended.");
            }
        }

        private void SuspendGossips()
        {
            _tickTimer.Change(System.Threading.Timeout.Infinite, System.Threading.Timeout.Infinite);
        }

        void UnicastLinkReceived(byte[] data)
        {
            var message = GossipMessage.FromBytes(data);
            Traces.Gems.TraceEvent(TraceEventType.Information, Traces.MessageReceived, "Received message from '{0}'", message.HostName);
            _suspectMatrix.Merge(message.Matrix);
        }

        private void tickTimerElapsed(object state)
        {
            SuspendGossips();

            try
            {
                _suspectMatrix.Tick();

                var hostNumber = 0;

                do
                {
                    hostNumber = _random.Next(0, _configuration.Hosts.Length);
                } while (!NodeIsAlive(hostNumber) || hostNumber == _configuration.HostPosition);

                var message = new GossipMessage(_suspectMatrix, _configuration.HostName.ToLowerInvariant());
                _unicastLink.SendAsync(_configuration.Hosts[hostNumber], message.ToBytes())
                .ContinueWith(delegate(Task<int> task)
                                  {
                                      if (task.IsFaulted)
                                      {
                                          Traces.Gems.TraceEvent(TraceEventType.Error, Traces.GeneralError,
                                                                 task.Exception.ToString());
                                      }
                                      else
                                      {
                                          Traces.Gems.TraceEvent(TraceEventType.Information,
                                                                                             Traces.MessageSent, "Sent message to '{0}'",
                                                                                             _configuration.Hosts[hostNumber]);
                                      }
                                                                    
                                                        });

            }
            catch (Exception ex)
            {
                Traces.Gems.TraceEvent(TraceEventType.Error, Traces.GeneralError,ex.ToString());
            }
            finally
            {
                _tickTimer.Change(_configuration.GossipInterval, System.Threading.Timeout.Infinite);
            }
        }

        private bool NodeIsAlive(int hostNumber)
        {
            return _liveList[hostNumber];
        }
    }
}
